Python3によるオブジェクト指向プログラミング - 合成と継承
著者:Leonardo Giordani - 20/08/2014 Updated on May 22, 2019
この記事はIPython Notebookとして公開されています。 委譲について
クラスがオブジェクトであるならば、タイプとインスタンスの違いは何でしょうか?
私が「私の猫」について話すとき、私は「動物」のサブタイプである「猫」の概念の具体的なインスタンスを参照しています。つまり、どちらもオブジェクトであるにもかかわらず、タイプは特殊化できるのに対し、インスタンスは特殊化できないのです。
通常、オブジェクトBがオブジェクトAの特殊化であると言われるのは以下の場合です。
BはAのすべての機能を持っている
Bは新しい機能を提供できる
Aのタスクの一部または全部を、Bが別の方法で実行できる。
これらのターゲットは非常に一般的で、どのようなシステムにも有効です。そして、既存のコンポーネントを最大限に再利用してこれらの目標を達成するための鍵となるのが、委任(Delegation) です。委譲とは、あるオブジェクトが最も得意とすることだけを実行し、それ以外のことは他のオブジェクトに任せることです。
委譲は、合成(composition) と 継承(inheritance) という2つの異なるメカニズムで実装することができます。残念なことに、OOP技術の柱として継承だけが挙げられることが多く、継承がより汎用的で基本的なメカニズムである委譲の実装であることが忘れられています。この2つの技術の名称は、明示的委譲(合成)と暗黙的委譲(継承)とした方が良いかもしれません。
繰り返しになりますが、合成や継承の話をするときは、行動的(behavioural)または構造的(structural)なデリゲーションに焦点を当てて話していることに注意してください。合成と継承の違いを考える別の方法として、オブジェクトが誰が要求を満たすことができるかを知っているのか、それともオブジェクトが要求を満たすものなのかを考えることができます。
多くの場合、合成はよりシンプルなシステムにつながり、保守性や変更性にもメリットがあります。
通常、合成は特別な構文を必要としない非常に汎用的な技術であると言われていますが、一方で、継承とそのルールは選択した言語に強く依存しています。実際には、Pythonの強い動的な性質が、この2つのテクニックの境界線を和らげています。
継承の啓示
Pythonでは、クラス継承の仕組みを使って、1つのクラスを1つ以上の異なるクラスの拡張として宣言することができます。子クラス(継承するクラス)は親クラス(継承されるクラス)と同じ内部構造を持ち、多重継承の場合には親クラス間で起こりうる衝突や再定義を管理するための非常に特別なルールがあります。継承の非常に簡単な例は
code: python
class SecurityDoor(Door):
pass
ここでは、現時点ではDoorクラスの完全なコピーである新しいクラスSecurityDoorを宣言しています。属性やメソッドにアクセスするとどうなるか調べてみましょう。まず、クラス SecurityDoor をインスタンス化します。
code: python
>> sdoor = SecurityDoor(1, 'closed')
最初にできることは、クラスの属性がグローバルで共有されているかどうかです。
code: python
>> SecurityDoor.colour is Door.colour
True
>> sdoor.colour is Door.colour
True
これは、Pythonがインスタンスメンバを解決しようとする際に、そのインスタンスが属するクラスを調べるだけでなく、親クラスも調査することを示しています。この場合、sdoor.color は SecurityDoor.color になり、それが Door.color になります。SecurityDoor は Door です。
また、__dict__ の中身を調べると、継承の仕組みを垣間見ることができます。
code: pytohn
>> sdoor.__dict__
{'number': 1, 'status': 'closed'}
>> sdoor.__class__.__dict__
mappingproxy({'__doc__': None, '__module__': '__main__'})
>> Door.__dict__
mappingproxy({'__dict__': <attribute '__dict__' of 'Door' objects>,
'colour': 'yellow',
'open': <function Door.open at 0xb687e224>,
'__init__': <function Door.__init__ at 0xb687e14c>,
'__doc__': None,
'close': <function Door.close at 0xb687e1dc>,
'knock': <classmethod object at 0xb67ff6ac>,
'__weakref__': <attribute '__weakref__' of 'Door' objects>,
'__module__': '__main__',
'paint': <classmethod object at 0xb67ff6ec>})
つまり、継承ツリーを介してクラスのメソッド呼び出しを解決するためにPythonが行うことの例は以下の通りです。
code: python
>> sdoor.__class__.__bases__0.__dict__'knock'.__get__(sdoor) <bound method type.knock of <class '__main__.SecurityDoor'>>
>> sdoor.knock
<bound method type.knock of <class '__main__.SecurityDoor'>>
これはあくまでも例であり、多重継承は考慮されていないことに注意してください。
それでは、いくつかのメソッドや属性をオーバーライドしてみましょう。Pythonでは、親クラスのメンバを子クラスで再定義するだけで、そのメンバをオーバーライド(Override)(再定義)することができます。
code: python
class SecurityDoor(Door):
colour = 'gray'
locked = True
def open(self):
if not self.locked:
self.status = 'open'
予想通り、オーバーライドされたメンバーは SecurityDoor クラスの __dict__ に存在しています。
code: python
>> SecurityDoor.__dict__
mappingproxy({'__doc__': None,
'__module__': '__main__',
'open': <function SecurityDoor.open at 0xb6fcf89c>,
'colour': 'gray',
'locked': True})
そのため、メンバをオーバーライドすると、親クラスのメンバではなく、子クラスのメンバが使用されます。これは、クラス階層をさかのぼったときに、前者が後者よりも先に見つかったからです。これは、Pythonがメソッドをオーバーライドしたときに親の実装を暗黙のうちに呼び出すことはないということも示しています。つまり、オーバーライドは暗黙の委任をブロックする方法なのです。
もし親の実装を呼び出したい場合は、明示的に行わなければなりません。前者の例では、次のように書くことができました。
code: python
class SecurityDoor(Door):
colour = 'gray'
locked = True
def open(self):
if self.locked:
return
Door.open(self)
この実装が正しく動作しているかどうかを簡単にテストすることができます。
code: python
>> sdoor = SecurityDoor(1, 'closed')
>> sdoor.status
'closed'
>> sdoor.open()
>> sdoor.status
'closed'
>> sdoor.locked = False
>> sdoor.open()
>> sdoor.status
'open'
しかし、このような形での明示的な親の委譲は大きく推奨されません。
1つ目の理由は、メソッドを呼び出す際に親クラスの名前を再度明示的に指定することで、非常に高い結合(Coupling)が発生するためです。結合とは、コンピュータサイエンスの用語では、システムの2つの部分を結びつけることを意味し、一方の変更が他方の部分に直接影響を与えるようにすることで、通常はできる限り避けます。この場合、もし新しい親クラスを使うことにしたら、そのクラスを呼び出すすべてのメソッドに変更を手動で伝播させなければなりません。さらに、Pythonではクラス階層が動的に(つまり実行時に)変更されることがあるので、このような形での明示的な委譲は煩わしいだけでなく、間違っている可能性もあります。
2つ目の理由は、一般的に多重継承を扱う必要があり、どの親クラスがオーバーライドしているメソッドのオリジナルの形を実装しているかが事前にわからないことです。
これらの問題を解決するために、Pythonは super() という組み込み関数を提供しています。この関数はクラス階層を登り、呼び出されるべき正しいクラスを返します。super() を呼び出すための構文は以下の通りです。
code: python
class SecurityDoor(Door):
colour = 'gray'
locked = True
def open(self):
if self.locked:
return
super().open()
super() の出力は、正確にはDoorクラスではありません。super() は、<super: <class 'SecurityDoor'>, <SecurityDoor object>> という内部表現のsuperオブジェクトを返します。しかし、このオブジェクトは親クラスのように動作するので、カスタムの性質を無視して、この場合のDoorクラスと同じように使用することができます。
合成の役割
合成とは、あるオブジェクトが他のオブジェクトを知っていて、そのオブジェクトに明示的にタスクを委ねることです。継承が暗黙的であるのに対し、合成は明示的です。しかし、Pythonでは、これよりもはるかに興味深いことがあります。)
まず最初に、古典的な合成を実装してみましょう。これは単純に、あるオブジェクトを属性として他のオブジェクトの一部にするものです。
code: python
class SecurityDoor:
colour = 'gray'
locked = True
def __init__(self, number, status):
self.door = Door(number, status)
def open(self):
if self.locked:
return
self.door.open()
def close(self):
self.door.close()
合成の主な目的は、オブジェクト間の結合を緩和することです。この小さな例では、SecurityDoor がオブジェクトであり、Door ではないことを示しています。この非常に単純な例では、Door も SecurityDoor も大きなクラスではありませんが、実際のシステムでは、オブジェクトは非常に複雑になります。
委譲の概念はメソッドにのみ適用され、属性には適用されないので、構成された SecurityDoor は color 属性を再定義しなければならないのではないでしょうか?
しかし、そうではありません。Python はオブジェクトを操作するために非常に高度なインダイレクトを提供しており、属性アクセスは最も便利なものの一つです。既にお分かりのように、属性へのアクセスは __getattribute__() と呼ばれる特別なメソッドによって規定されており、オブジェクトの属性にアクセスするたびに呼び出されます。しかし、__getattribute__() をオーバーライドするのはやりすぎです。これは非常に複雑なメソッドであり、属性にアクセスするたびに呼び出されるため、変更すると全体が遅くなります。
属性へのアクセスを委任するために利用しなければならないメソッドは __getattr__() で、これは要求された属性がオブジェクトに見つからないときに呼び出される特別なメソッドです。このメソッドは、要求された属性がオブジェクトに見つからない場合に呼び出される特別なメソッドです。先ほどの例は以下のようになります。
code: python
class SecurityDoor:
locked = True
def __init__(self, number, status):
self.door = Door(number, status)
def open(self):
if self.locked:
return
self.door.open()
def __getattr__(self, attr):
return getattr(self.door, attr)
__getattr__() を使用すると、継承と合成の境界線が曖昧になります。
code: python
class ComposedDoor:
def __init__(self, number, status):
self.door = Door(number, status)
def __getattr__(self, attr):
return getattr(self.door, attr)
この最後の例が示すように、__getattr__() を使ってすべてのメンバーのアクセスを委任することは非常に簡単です。getattr()は__getattr__() とは異なるので注意が必要です。getattr(obj, 'someattr') は obj.someattr と同じですが、属性名が文字列に含まれているため、これを使用しなければなりません。
合成は、アクセスを選択的に委譲することができ、一部の属性やメソッドをマスクすることもできるので、委譲を管理するための優れた方法を提供しますが、継承ではできません。また、Pythonでは、多くのオブジェクトを別のオブジェクトの中に入れたときに生じる可能性のあるメモリ問題を回避することができます。Pythonはすべてのものをその参照を通して、つまり、オブジェクトのメモリ上の位置へのポインタを通して扱うので、属性のサイズは一定で、非常に限られたものになります。
映画のトリビア
セクションのタイトルは次の映画に由来しています。
The Cannonball Run (1981): The Delegation Run / 委譲について
run には、言葉がある様態で「話される」、「書かれる」という使い方もあることから
Apocalypse Now (1979): Inheritance Now / 継承の啓示
単純に訳すのであれば now に注目するだけですが、映画タイトルと韻を意識して「啓示」としています。
この映画タイトルの邦題は「地獄の黙示録」
Enter the Dragon (1973): Enter the Composition / 合成の役割
映画タイトルの意味で言えば侵入が妥当ですが、enter には仕事で「役割を持つ」という意味もあることから。
この映画の邦題は「燃えよドラゴン」という、言葉を超越した訳になっています。
参考資料
Python3によるオブジェクト指向プログラミングシリーズ
日本語訳:Python3によるオブジェクト指向プログラミング - 合成と継承